package gu.sql2java;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import com.google.common.primitives.Primitives;

/**
 * implementation of {@link IBeanConverter} by reflect<br>
 * generic type converter between L and R <br>
 * @author guyadong
 * @param <L> left type extends BaseBean 
 * @param <R> right type
 *
 */
public class BeanConverter<L extends BaseBean,R> extends IBeanConverter.AbstractHandle<L,R>  implements Constant{
	static final String GET_INITIALIZED = "getInitialized";
	static final String SET_INITIALIZED = "setInitialized";
	static final String GET_MODIFIED = "getModified";
	static final String SET_MODIFIED = "setModified";
	static final String SET_NEW = "setNew";
	static final String IS_NEW = "isNew";
	private final Map<String,Method> methods = new Hashtable<String,Method>();
	private final Map<String,Integer> rightIndexs = new Hashtable<String,Integer>();
	private final Map<String, Class<?>> setterParams = new Hashtable<String,Class<?>>();
	private final RowMetaData metaData;
	private boolean bitCheck(String name,int...bits){
		Integer id = rightIndexs.get(name);
		return (null == id)?false:bitCheck(id.intValue(),bits);
	}
	private int[] bitOR(String name,int... bits){
		return bitOR(rightIndexs.get(name),bits);
	}
	private void getGetter(String name){
		try{
			methods.put(name,rightType.getMethod(name));
		}catch(NoSuchMethodException e){}
	}
	private void getSetter(String name, Class<?>...types) throws NoSuchMethodException{
		for(Class<?>paramType:types){
			try{
				methods.put(name,rightType.getMethod(name,paramType));
				setterParams.put(name, paramType);
				return;
			}catch(NoSuchMethodException e){
				continue;
			}
		}
		throw new NoSuchMethodException();
	}
	private void getSetterNoThrow(String name, Class<?>...types){
		try{
			getSetter(name,types);
		}catch(NoSuchMethodException e){}
	}
	/**
	 * constructor
	 * @param leftClass
	 * @param rightClass
	 * @param javaFields a comma splice string,including all field name of R,<br>
	 *                   if null or empty, use default string:{@link RowMetaData#columnJavaNames}
	 */
	public BeanConverter (Class<L> leftClass, Class<R> rightClass,String javaFields){
		super(leftClass,rightClass);
		this.metaData = RowMetaData.getMetaData(leftClass);
		init(javaFields);
	}
	/** @see #BeanConverter(Class,Class,String) */
	public BeanConverter (Class<L> leftClass, Class<R> rightClass){
		this(leftClass,rightClass,null);
	}
	private void init(String javaFields){
		List<String>rightFields;
		if(null == javaFields || javaFields.isEmpty()){
			rightFields = metaData.columnJavaNames;
		}else{
			rightFields = Arrays.asList(javaFields.split(","));
		}
		for(int i = 0 ; i < rightFields.size(); ++i){
			String field = rightFields.get(i).trim();
			if(!field.matches("\\w+")){
				throw new IllegalArgumentException("invalid 'javaFields':" + javaFields);
			}
			rightIndexs.put(field,i);
		}
		try{
			methods.put(IS_NEW,rightType.getMethod(IS_NEW));
			methods.put(GET_INITIALIZED,rightType.getMethod(GET_INITIALIZED));
			getSetter(SET_NEW,boolean.class);
			if(rightIndexs.size() > STATE_BIT_NUM){
				getSetter(SET_INITIALIZED,int[].class,List.class);
			}else{
				getSetter(SET_INITIALIZED,int.class);
			}
			getGetter(GET_MODIFIED);
			if(rightIndexs.size() > STATE_BIT_NUM){
				getSetter(SET_MODIFIED,int[].class,List.class);
			}else{
				getSetter(SET_MODIFIED,int.class);
			}
		}catch(NoSuchMethodException e){
			throw new RuntimeException(e);
		}

		for(int i =0; i<metaData.columnCount; ++i){
			getGetter(metaData.getterMethods.get(i).getName());
			Method setter = metaData.setterMethods.get(i);
			Class<?> type = setter.getParameterTypes()[0];
			if(type.isPrimitive()){
				if(Date.class.isAssignableFrom(type)){
					getSetterNoThrow(setter.getName(),type,long.class);           
				}
				else{
					getSetterNoThrow(setter.getName(),type,Primitives.unwrap(type));
				}
			}else if(ByteBuffer.class.isAssignableFrom(type) || byte[].class.isAssignableFrom(type)){
				getSetterNoThrow(setter.getName(),type,ByteBuffer.class,byte[].class);
			} else {
				getSetterNoThrow(setter.getName(),type);                    
			}
		}
	}
	@Override
	protected void doFromRight(L left, R right) {
		try{
			Method getterMethod;
			left.resetIsModified();
			int selfModified = 0;
			int[] initialized;
			int[] modified;
			if(rightIndexs.size() > STATE_BIT_NUM){
				initialized = (int[])methods.get(GET_INITIALIZED).invoke(right);
				modified = (int[])methods.get(GET_MODIFIED).invoke(right);
			}else{
				initialized = new int[]{(Integer)methods.get(GET_INITIALIZED).invoke(right)};
				modified = new int[]{(Integer)methods.get(GET_MODIFIED).invoke(right)};
			}

			for(int columnId = 0; columnId < metaData.columnCount; ++columnId){
				String columnName = metaData.columnNameOf(columnId);
				if(bitCheck(columnName,initialized)){
					String getterName = metaData.getterMethods.get(columnId).getName();
					if(null != (getterMethod = methods.get(getterName))){
						left.setValue(columnId, getterMethod.invoke(right));
						if(bitCheck(columnName,modified)){
							selfModified |= (1<<columnId);
						}
					}
				}
			}
			left.setNew((Boolean)methods.get(IS_NEW).invoke(right));
			left.setModified(selfModified);
		}catch(RuntimeException e){
			throw e;
		}catch(Exception e){
			throw new RuntimeException(e);
		}
	}

	@Override
	protected void doToRight(L left, R right) {
		try{
			Method setterMethod;
			int[] initialized = new int[(rightIndexs.size() + STATE_BIT_NUM - 1)>>STATE_BIT_SHIFT];
			int[] modified = new int[(rightIndexs.size() + STATE_BIT_NUM - 1)>>STATE_BIT_SHIFT];
			Arrays.fill(initialized, 0);
			Arrays.fill(modified, 0);
			for(int columnId = 0; columnId < metaData.columnCount; ++columnId){
				String columnName = metaData.columnNameOf(columnId);
				String setterName = metaData.setterMethods.get(columnId).getName();
				if(null != (setterMethod = methods.get(setterName)) && left.isInitialized(columnId)){
					try{
						setterMethod.invoke(right,cast(setterParams.get(setterName),left.getValue(columnId)));
						bitOR(columnName,initialized);
						if(left.isModified(columnId)){
							bitOR(columnName,modified);
						}
					}catch(NullCastPrimitiveException e){}
				}
			}
			// IGNORE field fl_device.create_time , controlled by 'general.beanconverter.tonative.ignore' in properties file
			/*
                if(null != (setterMethod = methods.get(Column.createTime.setter)) && left.checkCreateTimeInitialized()){
                    try{
                        setterMethod.invoke(right,cast(setterParams.get(Column.createTime.setter),left.getCreateTime()));
                        bitOR(Column.createTime.name(),initialized);
                        if(left.checkCreateTimeModified()){
                            bitOR(Column.createTime.name(),modified);
                        }
                    }catch(NullCastPrimitiveException e){}
                }
			 */
			// IGNORE field fl_device.update_time , controlled by 'general.beanconverter.tonative.ignore' in properties file
			/*
                if(null != (setterMethod = methods.get(Column.updateTime.setter)) && left.checkUpdateTimeInitialized()){
                    try{
                        setterMethod.invoke(right,cast(setterParams.get(Column.updateTime.setter),left.getUpdateTime()));
                        bitOR(Column.updateTime.name(),initialized);
                        if(left.checkUpdateTimeModified()){
                            bitOR(Column.updateTime.name(),modified);
                        }
                    }catch(NullCastPrimitiveException e){}
                }
			 */
			if(null != (setterMethod = methods.get(SET_MODIFIED))){
				if( initialized.length > 1){
					setterMethod.invoke(right,cast(setterParams.get(SET_MODIFIED),initialized));
				}else{
					setterMethod.invoke(right,initialized[0]);
				}
			}
			methods.get(SET_NEW).invoke(right,left.isNew());
			if( initialized.length > 1){
				methods.get(SET_INITIALIZED).invoke(right,cast(setterParams.get(SET_INITIALIZED),initialized));
				methods.get(SET_MODIFIED).invoke(right,cast(setterParams.get(SET_MODIFIED),modified));
			}else{
				methods.get(SET_INITIALIZED).invoke(right,initialized[0]);
				methods.get(SET_MODIFIED).invoke(right,modified[0]);
			}
		}catch(RuntimeException e){
			throw e;
		}catch(Exception e){
			throw new RuntimeException(e);
		}
	}
	private static final List<Long> toList(long[] array) {
		ArrayList<Long> result = new ArrayList<Long>(array.length);
		for (int i = 0; i < array.length; i++) {
			result.add(new Long(array[i]));
		}
		return result;
	}
	private static final long[] toPrimitive(List<Long> list) {        
		long[] dst = new long[list.size()];
		Long element;
		for (int i = 0; i < dst.length; i++) {
			if(null == (element = list.get(i))){
				throw new IllegalArgumentException("can't cast List<Long> to long[] because of null element");
			}
			dst[i] = element.longValue();
		}
		return dst;
	}
	/**
	 * {@code source}转为{@code type}指定的类型
	 * @param type destination type
	 * @param source  source object
	 * @return T instance
	 */
	@SuppressWarnings({ "unchecked" })
	static final <T> T cast(Class<T> type,Object source){
		try{
			if(null ==source && type.isPrimitive()){
				throw new NullCastPrimitiveException(String.format("can't convert null to primitive type %s",type.getSimpleName()));
			}
			return (T) source;
		}catch(ClassCastException cce){
			// long[] -> List  
			if(List.class.isAssignableFrom(type) && null != source && source instanceof long[]){
				return (T) toList((long[]) source);
			}
			// List -> long[]   
			if(long[].class == type && null != source && source instanceof List){
				return (T) toPrimitive( (List<Long>) source);
			}
			// Long -> Date
			if(java.util.Date.class.isAssignableFrom(type) && null != source && source instanceof Long){
				try {
					// call constructor,such as  java.util.Date#Date(long), java.sql.Time.Time(long)
					return type.getConstructor(long.class).newInstance(source);
				} catch (Exception e) {
					StringWriter writer = new StringWriter();
					e.printStackTrace(new PrintWriter(writer));
					throw new ClassCastException(writer.toString());
				}
			}
			// Date -> Long,long
			if( long.class == type || Long.class == type){
				if(null != source && source instanceof java.util.Date){
					Long time = ((java.util.Date)source).getTime();
					return (T)time;
				}
			}
			// byte[] -> ByteBuffer
			if(ByteBuffer.class == type && null != source && source instanceof byte[]){
				return (T) ByteBuffer.wrap((byte[]) source);
			}
			// ByteBuffer -> byte[]
			if(byte[].class == type && null != source && source instanceof ByteBuffer){
				return (T) Sql2javaSupport.getBytesInBuffer((ByteBuffer) source);
			}
			throw cce;
		}
	}
	static final boolean bitCheck(int index,int...bits){
		return 0 != (bits[index>>STATE_BIT_SHIFT]&(1<<(index&0x1f)));
	}
	static final int[] bitOR(int index,int... bits){
		bits[index>>STATE_BIT_SHIFT] |= (1<<(index&0x1f));
		return bits;
	}
	private static class NullCastPrimitiveException extends ClassCastException {
		private static final long serialVersionUID = 1L;
		NullCastPrimitiveException(String message) {
			super(message);
		}
	}
}

